Report array&callable pass as wrong param#5573
Report array&callable pass as wrong param#5573phpstan-bot wants to merge 8 commits intophpstan:2.1.xfrom
array&callable pass as wrong param#5573Conversation
VincentLanglet
left a comment
There was a problem hiding this comment.
No, this behavior is on purpose.
We don't want to report missingTypehint check for array&callable since we can infer it.
But we should still report passing array&callable when passed to a function expecting array.
For instance we already report passing array&callable to array{string, string} (because it could be array{object|string, string}).
array&callable intersections in MissingTypehintCheckarray&callable pass as wrong param
VincentLanglet
left a comment
There was a problem hiding this comment.
Use another file bug-14549-bis.php for your tests
And add test with callable&array passed to a method expecting constant array
- array{string, string} => Error
- array{object|string, string} => No error
- array{object|string, string, string} => Error
|
Done. Here's what I changed:
|
VincentLanglet
left a comment
There was a problem hiding this comment.
I can reproduce a similar issue with array<mixed>&hasOffsetValue(1,1) which seems acceptedBy array<string>.
Maybe IntersectionType::isAccepted implementation is wrong.
I wonder if lazyMaxMin is the right check
When looking for A&B isAcceptedBy C
If C does not accept A, A&B cannot be accepted.
What about a strategy
- If a NO => Return No
- Else returns the Max (Maybe & Yes)
What would be the impact of such change ?
|
I processed this review but have nothing to report. |
|
Done. Here's a summary of what I changed: The problem: The reviewer pointed out that the PR's approach of special-casing Root cause: The fix: Replace the This approach:
|
| /** | ||
| * @param callable&array $task | ||
| */ | ||
| public function doBaz(array $task): void | ||
| { | ||
| } |
There was a problem hiding this comment.
Done. Removed the doBaz() method from bug-14549.php and its corresponding test expectation in MissingMethodParameterTypehintRuleTest.php. All 11,971 tests pass and make phpstan reports no errors.
|
This seems to work but I wonder if I and the bot are not making a mistake by confusing accepts and isSuperTypeOf... And there is multiple integration test failure I'll need to check I'll need to take more time on it, just wanted to know if it's inspire you @staabm ? |
… intersections in `MissingTypehintCheck` - In `MissingTypehintCheck::getIterableTypesWithMissingValueTypehint()`, the special-case block for `array&callable` intersection types was skipping the array part entirely, never checking it for missing value types - Added a check for the array inner type's iterable value type before skipping it, so `callable-array`, `callable&array`, and `array&callable(...)` now correctly report `missingType.iterableValue` when the array has no value type - Updated test expectations for `doIntersection()` (gains a third error for the outer array) and `doFoo()` (now reports missing iterable value type) - Added new test case `doBaz()` with explicit `callable&array` parameter - All consumers of `MissingTypehintCheck` (method/function parameters, return types, properties, constants, @var tags, @phpstan-assert) are automatically fixed since they share the same check
…ngTypehintCheck The missing typehint suppression for array&callable is intentional because PHPStan can infer the value type (object|non-falsy-string). Reverts the MissingTypehintCheck changes and their test expectations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rray&callable When an array&callable intersection is checked for acceptance, the raw ArrayType(mixed, mixed) component was used, causing array<int> to incorrectly accept array&callable. Now the array component uses the narrowed key/value types (int<0,1>, object|non-falsy-string) so the acceptance check correctly rejects incompatible array types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d constant array test cases
Test callable&array passed to methods expecting constant array types:
- array{string, string} => Error (object at index 0 is not string)
- array{object|string, string} => No error (matches callable-array shape)
- array{object|string, string, string} => Error (callable-array has 2 elements, not 3)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d of special-casing callable&array Replace the callable&array-specific narrowing logic with a general approach: after the standard lazyMaxMin check, if it returns Yes, verify with isSuperTypeOf that the accepting type is actually a supertype of the full intersection. This catches cases where MixedType's accepts-everything behavior causes false acceptances (e.g. array<int> falsely accepting array<mixed>&hasOffsetValue or array<mixed>&callable intersections). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
e3e1b99 to
3afa0ac
Compare
|
to fix this one, I would investigate why https://phpstan.org/r/d89df38c-c43c-49fe-bcdc-03a29f285690 reports
and than would try to check how the relevant logic can be applied to saying that and looking at phpstan-src/src/Rules/FunctionCallParametersCheck.php Lines 390 to 412 in 6191283 Type->accepts() or Type->isAcceptedBy() implementations.
looking deeper my feeling is the problem needs a fix in |
|
another thing which could work is diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php
index 777343626..124405a14 100644
--- a/src/Type/ArrayType.php
+++ b/src/Type/ArrayType.php
@@ -112,11 +112,9 @@ class ArrayType implements Type
}
return $result;
- }
-
- if ($type instanceof ArrayType) {
- return $this->getItemType()->accepts($type->getItemType(), $strictTypes)
- ->and($this->keyType->accepts($type->keyType, $strictTypes));
+ } else if ($type->isArray()->yes()) {
+ return $this->getIterableValueType()->accepts($type->getIterableValueType(), $strictTypes)
+ ->and($this->keyType->accepts($type->getIterableKeyType(), $strictTypes));
}
return AcceptsResult::createNo(); |
|
It's not 100% related to callable because the issue exist with HasOffsetType and HasOffsetValueType too Array of string For instance |
ok, than its definitly in IntersectionType (which is the type all your examples have in common) |
f8560a4 to
3afa0ac
Compare
VincentLanglet
left a comment
There was a problem hiding this comment.
Add IntersectionTypeTest with
- array&callable isAcceptedBy array
- array&callable isAcceptedBy constantArray
- array&hasOffsetValue isAcceptedBy array
- array&hasOffsetValue isAcceptedBy constantArray
In both success and failure cases (which wasn't fixed before).
Does the issue exists for another method ? (Maybe isSubtypeOf ?)
…lable and array&hasOffsetValue Tests cover both success and failure cases: - array&callable isAcceptedBy array/constantArray - array&hasOffsetValue isAcceptedBy array/constantArray - array&callable isSubTypeOf array - array&hasOffsetValue isSubTypeOf array The isSubTypeOf tests confirm that the issue does NOT exist there — isSubTypeOf correctly returns No for incompatible types (e.g. array&callable is not a subtype of array<int>, and array&hasOffsetValue(3,int) is not a subtype of array<string>). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Here's a summary of what I did: Added
Answer to the reviewer's question: The issue does NOT exist in |
- Make test comments more descriptive by including the accepting type details - Fix last hasOffsetValue failure test to use offset 0 (matching the constant array's offsets) so it properly tests type incompatibility, and update expected result to No since int at offset 0 conflicts with string Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VincentLanglet
left a comment
There was a problem hiding this comment.
This PR produce the new following errors in E2E tests
-
"Method PHPStan\Type\BeberleiAssert\AssertHelper::getExpressionResolvers() should return array<string, callable(PHPStan\Analyser\Scope, PhpParser\Node\Arg ...): (array{PhpParser\Node\Expr|null, PhpParser\Node\Expr|null}|PhpParser\Node\Expr|null)> but returns non-empty-array."
-
"Parameter #1 $generator of method Nette\DI\ContainerLoader::load() expects callable(Nette\DI\Compiler): (string|null), Closure(Nette\DI\Compiler): void given."
-
"Method Shopware\Core\Kernel::getKernelParameters() should return array<string, array<string, mixed>|bool|float|int|string|UnitEnum|null> but returns array<string, array|bool|float|int|string|UnitEnum|null>."
-
"Parameter #1 $type of method Nette\DI\Definitions\ServiceDefinition::setType() expects class-string|null, string given."
WDYT of them ? Which ones are regression ?
|
Pushed. Here's my analysis of the E2E errors and what I changed: Analysis of E2E errorsError 1 (BeberleiAssert): Errors 2-4 (Nette ContainerLoader, Shopware, Nette ServiceDefinition) — Likely NOT caused by this PR. None of these types appear to involve
What changedReplaced the blanket |
VincentLanglet
left a comment
There was a problem hiding this comment.
Remove all the IntersectionTypeTest and remove the change in IntersectionType for now.
Instead look for a fix in ArrayType::accepts.
Seems like ConstantArrayType::accepts already have a special behavior
if ($type instanceof CompoundType && !$type instanceof IntersectionType) {
So maybe we should handle IntersectionType differently in ArrayType ?
|
Pushed. Here's what I changed: Removed from Removed from Added to Updated baseline: This approach preserves the existing |
fd77c6b to
9759075
Compare
|
Integration failure are valid to me ; for instance
|
Summary
When a parameter, return type, or property was declared with an
array&callableintersection type (including thecallable-arrayshorthand),MissingTypehintCheckdid not report the missing iterable value type for the array part. This meant users were not warned about the imprecisearray<mixed>in these intersections, and downstream argument type checks couldn't catch mismatches (e.g., passingarray&callabletoarray<int>).Changes
src/Rules/MissingTypehintCheck.php: In the special-case block forarray&callableintersections (lines 76–96), added a check on each array inner type'sgetIterableValueType(). If the value type is implicitmixed, it is now added to the missing-typehint list. The non-array parts (callable) are still traversed for their own inner type checks (e.g., callable parameter/return arrays).doBaz()with@param callable&arrayintests/PHPStan/Rules/Methods/data/bug-14549.phptests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php:doFoo()(callable-array) now reportsmissingType.iterableValuefor its array partdoIntersection()(array&callable(array): array) now reports a thirdmissingType.iterableValuefor the outer arraydoBaz()(callable&array) reports bothmissingType.iterableValueandmissingType.callableRoot cause
MissingTypehintCheck::getIterableTypesWithMissingValueTypehint()had a special-case block (lines 76–92) for intersection types that are both callable and array. This block extracted the non-array parts and traversed only those, completely skipping the array parts. The array parts were never checked for missing value types, socallable-array/callable&arraysilently passed the check even though their array part has implicitmixedvalues.The fix adds the missing value type check for each array inner type within the existing special-case block, before the
continuethat skips it.Analogous cases probed
Since the fix is in the shared
MissingTypehintCheckclass, all consumers are automatically fixed:MissingMethodParameterTypehintRule) — tested and verifiedMissingFunctionParameterTypehintRule) — shares the same check, tests passMissingMethodReturnTypehintRule) — shares the same check, tests passMissingFunctionReturnTypehintRule) — shares the same check, tests passMissingPropertyTypehintRule) — shares the same check, tests passMissingClassConstantTypehintRule) — shares the same check, tests passgetIterableTypesWithMissingValueTypehint()methodNon-array callable intersections (e.g.,
Traversable&callable) don't hit this code path (the condition requiresisArray()->yes()), and they are already handled correctly by the general iterable check below.Test
testBug14549inMissingMethodParameterTypehintRuleTestverifies:callable-arrayreports missing iterable value typearray&callable(array): arrayreports missing value type for all three arrays (outer + callable param + callable return)callable&arrayreports both missing iterable value type and missing callable signatureFixes phpstan/phpstan#14549